using System;
using System.Collections.Generic;
using System.Linq;
using IWMAG.Scripting.AST;
using IWMAG.Scripting.AST.Declarations;
using IWMAG.Scripting.AST.ObjectModel;
using IWMAG.Scripting.AST.Operators;
using IWMAG.Scripting.AST.Statements;
using IWMAG.Scripting.AST.TerminalExpressions;
using IWMAG.Scripting.Types;
using IWMAG.Scripting.Compiler.UserInfo;

namespace IWMAG.Scripting.Compiler {
    internal partial class Parser {
        protected ProgramDeclaration Program() {
            ProgramDeclaration program = new ProgramDeclaration();

            while (Ptr < Lexemes.Count) {
                int typelen = ScanPotentialType();
                if (Match("async") || Match("sync") || (Match(typelen, Token.Ident) && Match(typelen + 1, "("))) {
                    program.Functions.Add(Function());
                } else if (Match(typelen, Token.Ident)) {
// todo(sandy): enforce that these don't call async functions
                    program.Variables.Add(VarDecl());
                    if (Match("=>")) {
                        ErrorList.Add(new CompileError2(Rules.GlobalVariablesCannotBeDeclaredAsLateBound, Here.Line, Here.Column));
                        throw new ParseException();
                    }
                    Assert(";"); Ptr++;
                } else if (Match("alias")) {
                    program.Aliases.Add(Alias());
                } else {
                    ErrorList.Add(new CompileError2(Rules.InvalidTopLevelDeclaration, Here.Line, Here.Column, null, Here.ToString()));
                    throw new ParseException();
                }
            }

            bool foundError = false;

            var allDuplicateVariables = program.Variables.GroupBy(
                (a) => a.Name).Where((a) => a.Count() > 1);
            if (allDuplicateVariables.Count() > 0) {
                foundError = true;
                foreach (var duplicateSet in allDuplicateVariables) {
                    foreach (var allExceptFirst in duplicateSet.Skip(1)) {
                        allExceptFirst.SendError(ErrorList, Rules.RedeclaredVariableInScope, allExceptFirst.Name);
                    }

                }
            }

            var allDuplicateFunctions = program.Functions.GroupBy(
             (a) => a.Signature.Name).Where((a) => a.Count() > 1);
            if (allDuplicateFunctions.Count() > 0) {
                foundError = true;
                foreach (var duplicateSet in allDuplicateFunctions) {
                    foreach (var allExceptFirst in duplicateSet.Skip(1)) {
                        allExceptFirst.SendError(ErrorList, Rules.RedeclaredFunction, allExceptFirst.Signature.Name);
                    }

                }
            }

            if (foundError) {
                throw new ParseException();
            }

            return program;
        }

        protected Signature FunctionSignature() {
            Lexeme l = Here;
            bool asyncqualified = false;
            if (Match("async")) {
                Ptr++;
                asyncqualified = true;
            } else if (Match("sync")) {
                Ptr++;
            }
            CodeType type = ParseType();
            string ident = Here.StringData; Ptr++;
            Assert("("); Ptr++;
            List<VariableDeclaration> param = new List<VariableDeclaration>(Parameters());
            Assert(")"); Ptr++;
            Signature s = new Signature(ident, type, param);
            s.IsAsync = asyncqualified;
            return s;
        }

        protected FunctionDeclaration Function() {
            return new FunctionDeclaration(Here, FunctionSignature(), Statement());
        }

        protected IfStatement If() {
            Lexeme l = Here;
            Assert("if"); Ptr++;
            Assert("("); Ptr++;
            CodeExpression cond = Expression();
            Assert(")"); Ptr++;
            CodeStatement stmnt = Statement(), estmnt = null;
            if (Match("else")) {
                Ptr++;
                estmnt = Statement();
            }
            return new IfStatement(l, cond, stmnt, estmnt);
        }

        protected CodeExpression Expression() {
            return ExprPrec0();
        }

        protected CodeStatement Statement() {
            Lexeme l = Here;
            if (Match("if")) {
                return If();
            } else if (Match("while")) {
                return While();
            } else if (Match("do")) {
                return Do();
            } else if (Match("foreach")) {
                return Foreach();
            } else if (Match("for")) {
                return For();
            } else if (Match("{")) {
                return Block();
            } else if (Match("alias")) {
                return Alias();
            } else if (Match("return")) {
                Ptr++;
                CodeExpression expr = !Match(";") ? Expression() : null;
                Assert(";"); Ptr++;
                return new ReturnStatement(l, expr);
            } else if (Match("break")) {
                Ptr++;
                CodeExpression expr = !Match(";") ? Expression() : null;
                Assert(";"); Ptr++;
                return new BreakStatement(l, expr);
            } else if (Match("continue")) {
                Ptr++;
                CodeExpression expr = !Match(";") ? Expression() : null;
                Assert(";"); Ptr++;
                return new ContinueStatement(l, expr);
            } else if (Match("wait")) {
                Ptr++;
                CodeExpression expr = Expression();
                Assert(";"); Ptr++;
                return new WaitStatement(l, expr);
            } else if (Match("begin")) {
                Ptr++;
                Assert(1, "(");
                CodeExpression expr = ExprPostfix();
                if (!(expr is CallOperator)) {
                    ErrorList.Add(new CompileError("Begin statement must be followed by a function call.", Here.Line, Here.Column));
                    throw new ParseException();
                }
                Assert(";");
                return new BeginStatement(l, expr as CallOperator);
            } else if (Match("log")) {
                Ptr++;
                CodeExpression expr = Expression();
                Assert(";"); Ptr++;
                return new LogStatement(l, expr);
            } // these aren't available in a non global scope
            /* else if (Match("import")) {
                return Import();
            }*/

            return ForSafeStatement();
        }

        // statements safe to put into the first for clause
        protected CodeStatement ForSafeStatement() {
            Lexeme l = Here;
            if (Match(ScanPotentialType(), Token.Ident)) {
                VariableDeclaration decl = VarDecl();
                if (Match("=>")) {
                    Ptr++;
                    InlineStatements stmnts = new InlineStatements(l);
                    stmnts.Statements.Add(new VariableDeclarationStatement(l, decl));
                    stmnts.Statements.Add(new ExpressionStatement(l, new LateBindOperator(l, new VariableExpression(l, decl.Name), Expression())));
                    return stmnts;
                }
                Assert(";"); Ptr++;
                return new VariableDeclarationStatement(l, decl);
            } else if (Match(";")) {
                Ptr++;
                return new NullStatement(l);
            }

            CodeExpression expr = Expression();
            Assert(";"); Ptr++;
            return new ExpressionStatement(l, expr);
        }

        protected AliasStatement Alias() {
            Lexeme l = Here;
            Assert("alias"); Ptr++;
            CodeType type = ParseType();
            CodeType alias = ParseType();
            Assert(";"); Ptr++;
            return new AliasStatement(l, type, alias);
        }

        protected VariableDeclaration VarDecl() {
            Lexeme l = Here;
            CodeType type = ParseType();
            string name = Here.StringData; Ptr++;
            if (Match("=")) {
                Ptr++;
                return new VariableDeclaration(l, type, name, Expression());
            }
            return new VariableDeclaration(l, type, name);
        }

        protected ForeachStatement Foreach() {
            Lexeme l = Here;
            Assert("foreach"); Ptr++;
            Assert("("); Ptr++;
            VariableDeclaration decl = VarDecl();
            Assert(";"); Ptr++;
            CodeExpression source = Expression();
            Assert(")"); Ptr++;
            return new ForeachStatement(l, decl, source, Statement());
        }

        protected ForStatement For() {
            Lexeme l = Here;
            Assert("for"); Ptr++;
            Assert("("); Ptr++;
            CodeStatement first = ForSafeStatement();
            CodeExpression cond = !Match(";") ? Expression() : new BoolExpression(Here, true);
            Assert(";"); Ptr++;
            CodeStatement after = new ExpressionStatement(Here, !Match(")") ? Expression() : new BoolExpression(Here, false));
            Assert(")"); Ptr++;
            return new ForStatement(l, first, cond, after, Statement());
        }

        protected CodeStatement Block() {
            BlockStatement block = new BlockStatement(Here);

            Assert("{"); Ptr++;
            while (!Match("}")) {
                block.Statements.Add(Statement());
            } Ptr++;

            return block;
        }

        protected WhileStatement While() {
            Lexeme l = Here;
            Assert("while"); Ptr++;
            Assert("("); Ptr++;
            CodeExpression cond = Expression();
            Assert(")"); Ptr++;
            return new WhileStatement(l, cond, Statement());
        }

        protected DoStatement Do() {
            Lexeme l = Here;
            Assert("do"); Ptr++;
            CodeStatement stmnt = Statement();
            Assert("while"); Ptr++;
            Assert("("); Ptr++;
            CodeExpression cond = Expression();
            Assert(")"); Ptr++;
            Assert(";"); Ptr++;
            return new DoStatement(l, stmnt, cond);
        }

        protected CodeExpression ExprPrec0() {
            CodeExpression p1 = ExprPrec1();
            if (Match("=>")) {
                Lexeme l = Here;
                Ptr++;
                CodeExpression val = ExprPrec1();
                return new LateBindOperator(l, p1, val);
            }
            return p1;
        }

        protected CodeExpression ExprPrec1() {
            CodeExpression p1 = ExprPrec2();
            if (Match("=")) {
                Lexeme l = Here;
                Ptr++;
                CodeExpression val = ExprPrec1();
                return new AssignmentOperator(l, p1, val);
            }
            return p1;
        }

        protected CodeExpression ExprPrec2() {
            CodeExpression p1 = ExprPrec(0);
            if (Match("?")) {
                Lexeme l = Here;
                Ptr++;
                CodeExpression then = ExprPrec2();
                Assert(":"); Ptr++;
                CodeExpression els = ExprPrec2();
                return new TertOperator(l, p1, then, els);
            }

            return p1;
        }

        protected bool IsNextMatch(List<Tuple<string, Type>> operators) {
            foreach (Tuple<string, Type> op in operators) {
                if (Match(op.Item1)) {
                    return true;
                }
            }

            return false;
        }

        // matches everything in the operator mapping
        protected CodeExpression ExprPrec(int i) {
            CodeExpression p1 = i + 1 < Operators.Count ? ExprPrec(i + 1) : ExprPrefix();

            if (i < Operators.Count) {
                while (IsNextMatch(Operators[i])) {
                    foreach (Tuple<string, Type> op in Operators[i]) {
                        if (Match(op.Item1)) {
                            Lexeme l = Here;
                            Ptr++;
                            p1 = (CodeExpression) Activator.CreateInstance(op.Item2, l, p1, ExprPrec(i + 1));
                        }
                    }
                }
            }
            return p1;
        }

        protected CodeExpression ExprPrefix() {
            Lexeme l = Here;
            if (Match("++")) {
                Ptr++;
                return new PreIncOperator(l, ExprPostfix());
            } else if (Match("--")) {
                Ptr++;
                return new PreDecOperator(l, ExprPostfix());
            } else if (Match("!")) {
                Ptr++;
                return new NotOperator(l, ExprPrefix());
            } else if (Match("+")) {
                Ptr++;
                return ExprPostfix();
            } else if (Match("-")) {
                Ptr++;
                return new NegOperator(l, ExprPostfix());
            } else if (Match("~")) {
                Ptr++;
                return new BitNotOperator(l, ExprPrefix());
            }
            return ExprPostfix();
        }

        protected CodeExpression ExprPostfix() {
            CodeExpression p = Terminal();

            while (Match("(") || Match("[") || Match("++") || Match("--") || Match(".") || Match("[]")) {
                Lexeme l = Here;
                if (Match("(")) {
                    p = new CallOperator(l, p);
                    Ptr++;
                    ((CallOperator) p).Arguments.AddRange(CallArgs());
                    Assert(")"); Ptr++;
                } else if (Match("[]")) {
                    Ptr++;
                    return new NextIndexOperator(l, p);
                } else if (Match("[")) {
                    Ptr++;
                    p = new IndexOperator(l, p, Expression());
                    Assert("]"); Ptr++;
                } else if (Match("++")) {
                    Ptr++;
                    return new PostIncOperator(l, p);
                } else if (Match("--")) {
                    Ptr++;
                    return new PostDecOperator(l, p);
                } else if (Match(".")) {
                    Ptr++;
                    p = new MemberOperator(l, p, Terminal());
                }
            }

            return p;
        }

        protected CodeExpression Terminal() {
            Lexeme l = Here;
            if (Match(Token.StringValue)) {
                Ptr++;
                return new StringExpression(l, Rel(-1).StringData);
            } else if (Match(Token.IntValue)) {
                Ptr++;
                return new IntExpression(l, Rel(-1).IntData);
            } else if (Match(Token.RealValue)) {
                Ptr++;
                return new RealExpression(l, Rel(-1).RealData);
            } else if (Match(Token.True)) {
                Ptr++;
                return new BoolExpression(l, true);
            } else if (Match(Token.False)) {
                Ptr++;
                return new BoolExpression(l, false);
            } else if (Match(Token.Value)) {
                Ptr++;
                return new ValueExpression(l);
            } else if (Match(Token.Ident)) {
                Ptr++;
                return new VariableExpression(l, Rel(-1).StringData);
            } else if (Match("(")) {
                Ptr++;
                int len = ScanPotentialType();
                if (len != 0 && Match(len, ")")) {
                    CodeType type = ParseType();
                    Assert(")"); Ptr++;
                    return new CastOperator(l, type, ExprPrefix());
                }
                CodeExpression expr = Expression();
                Assert(")"); Ptr++;
                return expr;
            } else if (Match("const")) {
                Ptr++;
                Assert("("); Ptr++;
                CodeExpression expr = Expression();
                Assert(")"); Ptr++;
                return new ConstOperator(l, expr);
            } else if (Match("new")) {
                Ptr++;
                int type = ScanPotentialType();
                if (type == 0) {
                    ErrorList.Add(new CompileError("Operator new expects a type", Here.Line, Here.Column));
                    throw new ParseException();
                }
                NewOperator ctor = new NewOperator(l, ParseType());
                Assert("("); Ptr++;
                ctor.Arguments.AddRange(CallArgs());
                Assert(")"); Ptr++;
                return ctor;
            } else if (Match("{")) {
                Ptr++;
                ArrayExpression array = new ArrayExpression(l);
                array.Elements.AddRange(CallArgs());
                Assert("}"); Ptr++;
                return array;
            }

            ErrorList.Add(new CompileError("Non-parsable terminal expression. This implies you're using an unsupported operator or something.", Here.Line, Here.Column));
            throw new ParseException();
        }

        protected int ScanPotentialType(int rel = 0) {
            Ptr += rel;
            int dist = rel;

            if (Match(Token.Ident)) {
                while (Match(Token.Ident)) {
                    Ptr++;
                    dist++;
                    if (!Match(".")) break;
                    Ptr++;
                    dist++;
                    if (!Match(Token.Ident)) {
                        ErrorList.Add(new CompileError("Trailing . in typename", Here.Line, Here.Column));
                        throw new ParseException();
                    }
                }
            } else if (Match("string") || Match("void") || Match("real") || Match("bool") || Match("int")) {
                Ptr++;
                dist++;
            } else if (Match("var")) {
                Ptr -= rel;
                return 1 + rel;
            }

            while (Match("[]")) {
                Ptr++;
                dist++;
            }

            Ptr -= dist + rel;

            return dist;
        }

        protected CodeType ParseType() {
            CodeType res;

            if (Match(Token.Ident)) {
                List<string> s = new List<string>();
                while (Match(Token.Ident)) {
                    s.Add(Here.StringData);
                    Ptr++;
                    if (!Match(".")) break;
                    Ptr++;
                    if (!Match(Token.Ident)) {
                        ErrorList.Add(new CompileError("Trailing . in typename", Here.Line, Here.Column));
                        throw new ParseException();
                    }
                }
                res = new ReferenceType(s);
            } else if (Match("string")) {
                Ptr++;
                res = CodeType.String;
            } else if (Match("void")) {
                Ptr++;
                res = CodeType.Void;
            } else if (Match("bool")) {
                Ptr++;
                res = CodeType.Bool;
            } else if (Match("real")) {
                Ptr++;
                res = CodeType.Real;
            } else if (Match("int")) {
                Ptr++;
                res = CodeType.Int;
            } else if (Match("var")) {
                Ptr++;
                return CodeType.Auto;
            } else {
                ErrorList.Add(new CompileError("Impossible type!", Here.Line, Here.Column));
                throw new ParseException();
            }

            while (Match("[]")) {
                Ptr++;
                res = new ArrayType(res);
            }

            return res;
        }

        protected IEnumerable<VariableDeclaration> Parameters() {
            while (!Match(")")) {
                yield return VarDecl();
                if (!Match(")")) {
                    Assert(","); Ptr++;
                }
            }
            yield break;
        }

        protected IEnumerable<CodeExpression> CallArgs() {
            while (!Match(")") && !Match("}")) {
                yield return Expression();
                if (!Match(")") && !Match("}")) {
                    Assert(","); Ptr++;
                }
            }
            yield break;
        }
    }
}